/* Copyright 2009 Requirements Management System 
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.mags.remas.view.widgets;

import java.awt.geom.AffineTransform;  
import java.awt.geom.Point2D;
import java.io.InputStream;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.ControlAdapter;
import org.eclipse.swt.events.ControlEvent;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseListener;
import org.eclipse.swt.events.MouseMoveListener;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Cursor;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.ImageData;
//import org.eclipse.swt.graphics.ImageDataLoader;
import org.eclipse.swt.graphics.PaletteData;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.ScrollBar;

/**
 * 
 * @author Mario Gaitan
 *
 */
public class ImageCanvas extends Canvas {
	private Image sourceImage; 
	private Image originalImage;
	private Image shownImage; 
	private AffineTransform transform = new AffineTransform();
	private boolean cropMode = false;
	private Rectangle markRectangle = null;
	private Rectangle cropRectangle = null;
	private final Color selColor = getDisplay().getSystemColor(SWT.COLOR_BLUE);
	private final int CROPRECTANGLE_WIDTH = 30;
	private final int CROPRECTANGLE_HEIGHT = 30;
	private Point offset = null;
	private boolean resize = false;
	private Rectangle cropedRectangle;
	private float zoomFactor = 1.0f;
	
	public void setZoomFactor(float zoomFactor) {
		this.zoomFactor = zoomFactor;
	}
	public void setRotateFactor(int rotateFactor) {
		this.rotateFactor = rotateFactor;
	}
	private int rotateFactor = 0;

	public float getZoomFactor() {
		return zoomFactor;
	}
	public int getRotateFactor() {
		return rotateFactor;
	}
	public boolean isCropMode() {
		return cropMode;
	}
	public void crop() {
		if(originalImage!=null) {
			cropedRectangle = new Rectangle(markRectangle.x-(int)transform.getTranslateX(), markRectangle.y-(int)transform.getTranslateY(), markRectangle.width, markRectangle.height);
			updateImage();
		}
	}
	private Image crop(Image image, int x, int y, int w, int h) {
		Image src = new Image(getDisplay(), image.getImageData());
		Image cropImage = new Image(getDisplay(), w, h);
		if (x+w > src.getBounds().width) 
			w = src.getBounds().width - x;
		if (y+h > src.getBounds().height) 
			h = src.getBounds().height - y;
		GC cropGC = new GC(cropImage);
		cropGC.drawImage(src,
				x, y,
				w, h,
				0, 0,
				w, h);
		cropGC.dispose();
		src.dispose();
		return cropImage;
	}	
	public void setCropMode(boolean cropMode) {
		if(cropMode&&sourceImage!=null) {
			Rectangle si = sourceImage.getBounds();
			if(markRectangle==null)
				markRectangle = new Rectangle(si.x, si.y, CROPRECTANGLE_WIDTH, CROPRECTANGLE_HEIGHT);
			cropRectangle = markRectangle;
		}			
		this.cropMode = cropMode;
		redraw();
	}
	public void disposeImages() {
		if(originalImage!=null && !originalImage.isDisposed())
			originalImage.dispose();
		if(sourceImage != null && !sourceImage.isDisposed())
			sourceImage.dispose();
		if(shownImage != null && !shownImage.isDisposed())
			shownImage.dispose();
		originalImage = null;
		sourceImage = null;
		shownImage = null;
	}
	private Image loadImage(Object source) {
		disposeImages();
		if(source instanceof String)
			sourceImage = new Image(getDisplay(), (String)source);
		else if(source instanceof InputStream)
			sourceImage = new Image(getDisplay(), (InputStream)source);
		else if(source instanceof Image)
			sourceImage = ((Image)source);
		else
			throw new RuntimeException("source not supported");
		originalImage = new Image(getDisplay(), (ImageData)sourceImage.getImageData().clone());
		zoomFactor = 1.0f;
		cropMode = false;
		loadSourceImage();
		return sourceImage;
	}
	public Image loadImage(Image image) {
		return loadImage((Object)image);
	}
	public Image loadImage(String filename) {
		return loadImage((Object)filename);
	}
	public Image loadImage(InputStream inputStream) {
		return loadImage((Object)inputStream);
	}	
	public void dispose() {
		super.dispose();
		disposeImages();
	}
	public ImageCanvas(Composite parent) {
		this(parent, SWT.NULL);
	}
	public ImageCanvas(final Composite parent, int style) {
		super( parent, style|SWT.BORDER|SWT.V_SCROLL|SWT.H_SCROLL| SWT.NO_BACKGROUND);
		addControlListener(new ControlAdapter() {
			public void controlResized(ControlEvent event) {
				syncScrollBars();
			}
		});
		addPaintListener(new PaintListener() { 
			public void paintControl(final PaintEvent event) {
				paint(event.gc);
			}
		});
		addMouseListener(new MouseListener() {
			@Override
			public void mouseDoubleClick(MouseEvent e) {
			}
			@Override
			public void mouseDown(MouseEvent event) {
				if(markRectangle!=null) {
					if (markRectangle.contains (event.x, event.y)) {
						int deltax = Math.abs(event.x-markRectangle.x-markRectangle.width);
						int deltay = Math.abs(event.y-markRectangle.y-markRectangle.height);
						if(deltax>5||deltay>5) {
							Point point = new Point(event.x-markRectangle.x, event.y-markRectangle.y);
							offset = point;
						} else
							resize = true;
					}
				}
			}
			@Override
			public void mouseUp(MouseEvent e) {
				offset = null;
				resize = false;
				cropRectangle = markRectangle;
				redraw();
			}
		});
		final Cursor resizeCursor = new Cursor(getDisplay(),SWT.CURSOR_SIZESE);
		final Cursor moveCursor = new Cursor(getDisplay(),SWT.CURSOR_CROSS);
		final Cursor normalCursor = getCursor();
		addMouseMoveListener(new MouseMoveListener() {
			@Override
			public void mouseMove(MouseEvent event) {
				if(markRectangle!=null&&cropMode) {
					int deltax = event.x-markRectangle.x-markRectangle.width;
					int deltay = event.y-markRectangle.y-markRectangle.height;
					if(deltax>=-5&&deltax<=0&&deltay<=0&&deltay>=-5)
						setCursor(resizeCursor);
					else if(markRectangle.contains(event.x, event.y))
						setCursor(moveCursor);
					else
						setCursor(normalCursor);
					boolean redraw = false;
					if (offset!=null) {
						markRectangle.x = (event.x-offset.x)<0?0:(event.x-offset.x);
						markRectangle.y = (event.y-offset.y)<0?0:(event.y-offset.y);
						redraw = true;
					}		
					if(resize) {
						if(markRectangle.width+deltax>=CROPRECTANGLE_WIDTH)
							markRectangle.width = markRectangle.width+deltax;
						if(markRectangle.height+deltay>=CROPRECTANGLE_HEIGHT)
							markRectangle.height = markRectangle.height+deltay;
						redraw = true;
					}					
					if(redraw) {
						cropRectangle = null;
						redraw();
					}
					else
						cropRectangle = markRectangle;
				}
			}
		});
		initScrollBars();
	}
	private static Rectangle transformRectangle(AffineTransform af, Rectangle src){
		Rectangle dest= new Rectangle(0,0,0,0);
		src=absRect(src);
		Point p1=new Point(src.x,src.y);
		p1=transformPoint(af,p1);
		dest.x=p1.x; dest.y=p1.y;
		dest.width=(int)(src.width*af.getScaleX());
		dest.height=(int)(src.height*af.getScaleY());
		return dest;
	}
	private static Rectangle inverseTransformRectangle(AffineTransform af, Rectangle src){
		Rectangle dest= new Rectangle(0,0,0,0);
		src=absRect(src);
		Point p1=new Point(src.x,src.y);
		p1=inverseTransformPoint(af,p1);
		dest.x=p1.x; dest.y=p1.y;
		dest.width=(int)(src.width/af.getScaleX());
		dest.height=(int)(src.height/af.getScaleY());
		return dest;
	}
	private static Point transformPoint(AffineTransform af, Point pt) {
		Point2D src = new Point2D.Float(pt.x, pt.y);
		Point2D dest= af.transform(src, null);
		Point point=new Point((int)Math.floor(dest.getX()), (int)Math.floor(dest.getY()));
		return point;
	}
	private static Point inverseTransformPoint(AffineTransform af, Point pt){
		Point2D src=new Point2D.Float(pt.x,pt.y);
		try{
			Point2D dest= af.inverseTransform(src, null);
			return new Point((int)Math.floor(dest.getX()), (int)Math.floor(dest.getY()));
		}catch (Exception e){
			e.printStackTrace();
			return new Point(0,0);
		}
	}
	private static Rectangle absRect(Rectangle src){
		Rectangle dest= new Rectangle(0,0,0,0);
		if(src.width<0) { dest.x=src.x+src.width+1; dest.width=-src.width; } 
		else{ dest.x=src.x; dest.width=src.width; }
		if(src.height<0) { dest.y=src.y+src.height+1; dest.height=-src.height; } 
		else{ dest.y=src.y; dest.height=src.height; }
		return dest;
	}
	private void paint(GC gc) {
		Rectangle clientRect = getClientArea(); 
		if (sourceImage != null) {
			Rectangle imageRect = inverseTransformRectangle(transform, clientRect);
			int gap = 2;
			imageRect.x -= gap; imageRect.y -= gap;
			imageRect.width += 2 * gap; imageRect.height += 2 * gap;
			Rectangle imageBound = sourceImage.getBounds();
			imageRect = imageRect.intersection(imageBound);
			Rectangle destRect = transformRectangle(transform, imageRect);
			if (shownImage != null)
				shownImage.dispose();
			shownImage = new Image(getDisplay(), clientRect.width, clientRect.height);
			GC newGC = new GC(shownImage);
			newGC.setClipping(clientRect);
			newGC.drawImage(
				sourceImage,
				imageRect.x, imageRect.y,
				imageRect.width, imageRect.height,
				destRect.x, destRect.y,
				destRect.width, destRect.height);
			newGC.dispose();
			gc.drawImage(shownImage, 0, 0);
			if(cropMode&&markRectangle!=null) {
				gc.setLineWidth(2);
				gc.setLineStyle(SWT.LINE_DOT);
				gc.drawRectangle(markRectangle);				
				if(cropRectangle!=null) {
					gc.setBackground(selColor);
					gc.setAlpha(64);
					gc.fillRectangle(cropRectangle);
					gc.setAlpha(0xFF);
					Rectangle mark = new Rectangle(cropRectangle.x+cropRectangle.width-5
												 , cropRectangle.y+cropRectangle.height-5
												 , 5, 5);
					gc.fillRectangle(mark);
				}
			}
		} else {
			gc.setClipping(clientRect);
			gc.fillRectangle(clientRect);
			initScrollBars();
		}
	}
	public void rotate() {
		if(originalImage==null||originalImage.getImageData()==null)
			return ;
		rotateFactor = rotateFactor + 90;
		if(rotateFactor==360)
			rotateFactor = 0;
		updateImage();
	}
	public Image rotate(int rotateFactor, Image image) {
		ImageData src=image.getImageData();
		PaletteData srcPal=src.palette;
		PaletteData destPal;
		ImageData dest;
		if(srcPal.isDirect)
			destPal=new PaletteData(srcPal.redMask,srcPal.greenMask,srcPal.blueMask);
		else
			destPal=new PaletteData(srcPal.getRGBs());
		if((rotateFactor/90)%2==1)
			dest=new ImageData(src.height,src.width,src.depth,destPal);
		else
			dest=new ImageData(src.width,src.height,src.depth,destPal);
		for(int i=0;i<src.width;i++){
			for(int j=0;j<src.height;j++){
				int pixel=src.getPixel(i,j);
				if(rotateFactor==90||rotateFactor==-270)
					dest.setPixel(j,src.width-1-i,pixel);
				else if(rotateFactor==180)
					dest.setPixel(src.width-1-i,src.height-1-j,pixel);
				else if(rotateFactor==270||rotateFactor==-90)
					dest.setPixel(src.height-1-j,i,pixel);
			}
		}	
		return new Image(getDisplay(), dest);
	}
	public void updateImage() {
		if(zoomFactor==1.0f&&rotateFactor==0&&cropedRectangle==null) {
			reset();
			return ;
		}
		Image image = new Image(getDisplay(), (ImageData)originalImage.getImageData().clone());
		Image temp;
		if(zoomFactor!=1.0f) {
			temp = zoom(zoomFactor, image);
			image.dispose();
			image = temp;
		} 
		if(rotateFactor!=0) {
			temp = rotate(rotateFactor, image);
			image.dispose();
			image = temp;
		}
		if(cropedRectangle!=null) {
			temp = crop(image, cropedRectangle.x, cropedRectangle.y, cropedRectangle.width, cropedRectangle.height);
			image.dispose();
			image = temp;			
		}
		setImage(image);
	}
	private void initScrollBars() {
		ScrollBar horizontal = getHorizontalBar();
		horizontal.setEnabled(false);
		horizontal.addSelectionListener(new SelectionAdapter() {
			public void widgetSelected(SelectionEvent event) {
				scrollHorizontally((ScrollBar) event.widget);
			}
		});
		ScrollBar vertical = getVerticalBar();
		vertical.setEnabled(false);
		vertical.addSelectionListener(new SelectionAdapter() {
			public void widgetSelected(SelectionEvent event) {
				scrollVertically((ScrollBar) event.widget);
			}
		});
	}
	private void scrollHorizontally(ScrollBar scrollBar) {
		if (sourceImage == null)
			return;
		AffineTransform af = transform;
		double tx = af.getTranslateX();
		double select = -scrollBar.getSelection();
		af.preConcatenate(AffineTransform.getTranslateInstance(select - tx, 0));
		transform = af;
		syncScrollBars();
	}
	private void scrollVertically(ScrollBar scrollBar) {
		if (sourceImage == null)
			return;
		AffineTransform af = transform;
		double ty = af.getTranslateY();
		double select = -scrollBar.getSelection();
		af.preConcatenate(AffineTransform.getTranslateInstance(0, select - ty));
		transform = af;
		syncScrollBars();
	}
	public Image getSourceImage() {
		return sourceImage;
	}
	public void syncScrollBars() {
		if (sourceImage == null) {
			redraw();
			return;
		}
		AffineTransform af = transform;
		double sx = af.getScaleX(), sy = af.getScaleY();
		double tx = af.getTranslateX(), ty = af.getTranslateY();
		if (tx > 0) tx = 0;
		if (ty > 0) ty = 0;
		ScrollBar horizontal = getHorizontalBar();
		horizontal.setIncrement((int) (getClientArea().width / 100));
		horizontal.setPageIncrement(getClientArea().width);
		Rectangle imageBound = sourceImage.getBounds();
		int cw = getClientArea().width, ch = getClientArea().height;
		if (imageBound.width * sx > cw) { 
			horizontal.setMaximum((int) (imageBound.width * sx));
			horizontal.setEnabled(true);
			if (((int) - tx) > horizontal.getMaximum() - cw)
				tx = -horizontal.getMaximum() + cw;
		} else { 
			horizontal.setEnabled(false);
			tx = (cw - imageBound.width * sx) / 2; 
		}
		horizontal.setSelection((int) (-tx));
		horizontal.setThumb((int) (getClientArea().width));
		ScrollBar vertical = getVerticalBar();
		vertical.setIncrement((int) (getClientArea().height / 100));
		vertical.setPageIncrement((int) (getClientArea().height));
		if (imageBound.height * sy > ch) { 
			vertical.setMaximum((int) (imageBound.height * sy));
			vertical.setEnabled(true);
			if (((int) - ty) > vertical.getMaximum() - ch)
				ty = -vertical.getMaximum() + ch;
		} else { 
			vertical.setEnabled(false);
			ty = (ch - imageBound.height * sy) / 2; //center if too small.
		}
		vertical.setSelection((int) (-ty));
		vertical.setThumb((int) (getClientArea().height));
		af = AffineTransform.getScaleInstance(sx, sy);
		af.preConcatenate(AffineTransform.getTranslateInstance(tx, ty));
		transform = af;
		redraw();
	}
	public Image getImage() {
		return originalImage;
	}	
	public void setImage(Image image) {
		if (sourceImage != null)
			sourceImage.dispose();
		sourceImage = image;
		syncScrollBars();
	}
	public void fit() {
		if (originalImage == null)
			return;
		Rectangle imageBound = originalImage.getBounds();
		Rectangle destRect = getClientArea();
		double sx = (double) destRect.width / (double) imageBound.width;
		double sy = (double) destRect.height / (double) imageBound.height;
		double s = Math.min(sx, sy);
		long _s = (long)(s*10.0);
		zoomFactor = (float)(_s/10.0);
		updateImage();
	}
	public void reset() {
		setImage(new Image(getDisplay(),(ImageData)originalImage.getImageData().clone()));
		zoomFactor = 1.0f;
		rotateFactor = 0;
		cropedRectangle = null;
	}
	private void loadSourceImage() {
		if (sourceImage == null)
			return;
		transform = new AffineTransform();
		syncScrollBars();
	}
	public void zoomIn() {
		if(zoomFactor>2.0f)
			return ;
		zoomFactor = zoomFactor + 0.1f;
		updateImage();
	}
	public void zoomOut() {
		if(zoomFactor<0.1f)
			return ;
		zoomFactor = zoomFactor - 0.1f;
		updateImage();
	}
	public Image zoom(float zoomFactor, Image image) {
		if (image == null)
			return null;
		Rectangle rect = image.getBounds();
		int w = rect.width, h = rect.height;		
		Image scaledImage = new Image(getDisplay(), image.getImageData().scaledTo((int)(w * zoomFactor),(int)(h * zoomFactor)));
		return scaledImage;
	}
}
